下拉菜单组件:综合运用泛型&defineModel
之前实现的 AvatarMenu 中的下拉菜单是一个特化的业务组件。本节将其泛化,提炼为一个真正通用的 Dropdown 组件:通过泛型接受任意类型的数据项,通过 defineModel 实现激活项的双向绑定。这个组件综合运用了 TypeScript 泛型、Vue 3.4 的 defineModel、以及 Element Plus 的 Dropdown 组件。
泛型组件设计
为什么需要泛型
下拉菜单的数据项可能是多种类型:字符串、数字、对象。如果每种类型都写一个组件,会产生大量重复代码。泛型让一个组件处理所有类型:
// 使用泛型定义 Props
interface DropdownProps<T> {
data: T[]
// 泛型数据项数组
}
typescript
defineModel 的泛型支持
Vue 3.4 的 defineModel 支持泛型参数,可以指定 model 的类型:
const activeIndex = defineModel<number | string>('activeIndex', {
default: 0,
})
typescript
用户通过 v-model:active-index 绑定当前激活项的索引,组件在内部处理激活项的切换。
通用 Dropdown 组件实现
<!-- src/components/menu/dropdown.vue -->
<template>
<el-dropdown v-bind="dropdownProps" @command="handleCommand">
<slot />
<template #dropdown>
<el-dropdown-menu>
<template v-for="(item, index) in data" :key="index">
<el-divider
v-if="isDivider(item)"
class="my-0!"
/>
<el-dropdown-item
v-else
:command="index"
:class="{ 'is-active': activeIndex === index }"
>
<slot name="item" :item="item" :index="index">
{{ getLabel(item) }}
</slot>
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts" generic="T">
import type { DropDownMenuItem } from './types'
interface DropdownProps {
data: T[]
// ...其他属性
}
const props = defineProps<DropdownProps>()
const activeIndex = defineModel<number | string>('activeIndex')
const handleCommand = (command: number | string) => {
activeIndex.value = command
}
const getLabel = (item: T): string => {
if (typeof item === 'string' || typeof item === 'number') {
return String(item)
}
return (item as any).label ?? (item as any).value ?? String(item)
}
const isDivider = (item: T): boolean => {
return typeof item === 'object' && (item as any).key === 'divider'
}
</script>
vue
泛型的使用方式
泛型参数 T 由使用者传入的 data 数组自动推导:
// 使用字符串数组
const simpleData = ['选项一', '选项二', '选项三']
// T 推导为 string
// 使用对象数组
const objectData = [
{ key: 'profile', label: '个人中心' },
{ key: 'divider', label: '' },
{ key: 'logout', label: '退出登录' },
]
// T 推导为 { key: string; label: string }
typescript
scoped slot 的设计
通过 scoped slot 让用户自定义每个菜单项的渲染:
<Dropdown :data="menuData" v-model:active-index="activeIdx">
<template #default>
<el-avatar>U</el-avatar>
</template>
<template #item="{ item, index }">
<div class="flex items-center">
<Iconify v-if="item.icon" :icon="item.icon" class="mr-2" />
<span>{{ item.label }}</span>
</div>
</template>
</Dropdown>
vue
slot props 包含 item(当前数据项)和 index(索引),用户可以完全控制渲染内容。
本节小结
- 泛型组件:使用
<script setup lang="ts" generic="T">定义泛型组件,让数据类型由使用者决定。 - defineModel:通过
defineModel实现激活项的双向绑定,简化父子通信。 - scoped slot:提供
#item插槽让用户自定义菜单项渲染,保证组件的灵活性。 - divider 支持:内置分割线判断逻辑,数据中
key === 'divider'自动渲染分割线。
↑